New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
overlord,usersession: initial notifications of pending refreshes #9446
overlord,usersession: initial notifications of pending refreshes #9446
Conversation
b713034
to
489ea95
Compare
The new API allows snapd to notify userd, in each session, about a pending snap refresh. The notification carries the snap name and the amount of time remaining, before a forced refresh occurs. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
When refresh-app-awareness is enabled, snapd will not only issue snap warnings, but more importantly, send notifications to all the user sessions, carrying localized information about an upcoming snap refresh. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
489ea95
to
0e7e301
Compare
|
||
// TODO: silently ignore error returned when the notification server does not exist. | ||
// TODO: track returned notification ID and respond to actions, if supported. | ||
if _, err := notifySrv.SendNotification(msg); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also track the returned notification ID for as long as userd is alive, and use it to update existing notification, if one exists. This would ensure that the roster of persistent notifications only shows the current, up-to-date entry for each snap, not a separate entry per notification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of the nicer parts of the the new GNOME notification spec: it has client provided notification IDs, so we don't need to track server IDs to update/replace/withdraw old notifications.
// XXX: how are instances supported in the snap store, are they? | ||
|
||
// TODO: silently ignore error returned when the notification server does not exist. | ||
// TODO: track returned notification ID and respond to actions, if supported. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we planning to do actual interaction here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that this was literally designed last week I couldn't say.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to have interactivity for FDO notifications, the idle tracking code will need updating to keep the session agent alive until the notification is dismissed. That's more of a TODO item than something that needs to be done in this PR though.
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
I think this doesn't work the way the spec claims it to work. The notification sticks around until dismissed. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Thanks to Maciej for spotting that this runs with the state lock. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
I'm not sure if the wording of the warning explains the risks clearly. I would interpret "disruptions" as 'the application will suddenly close". Is that the correct and only risk of missing the deadline? I though refreshes would simply happen in the background. I thought that the application would keep running but some state and configuration might be lost the next time the application starts. Maybe something like "Close the app to avoid disruptions and data loss." is better? |
The application will not suddenly close, at least not in general. Some applications do observe issues related to the update of the apparmor profile.
Refreshes do happen in the background. This message informs the user that a refresh is possible but was postponed. Still, thank you for the feedback. I'm sure the text will still change before this is released to the public by default. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the code direction looks good to me, but I agree we need some more basic unit tests of this before we can merge it I think
overlord/snapstate/autorefresh.go
Outdated
path := app.DesktopFile() | ||
if _, err := os.Stat(path); err == nil { | ||
refreshInfo.BusyAppName = appName | ||
refreshInfo.BusyAppDesktopEntry = strings.SplitN(filepath.Base(path), ".", 2)[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this robust enough because DesktopFile will always end in ".desktop" and we don't allow "." in the names of desktop files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The desktop file is generated by us so yes. I think this is sensible.
This allows BusySnapError to be able to produce, by itself, PendingSnapRefreshInfo. I briefly experimented with a version that needs an externally-provided snap.Info, but it was more problematic as it could be called with the wrong info. Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
This arrangement is not perfect, as those span packages (the return type is from a different package) but this feels more natural than the previous choice. While cleaning up this code, add missing tests for this logic. Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
This will aid in testing and makes the code invoking it more readable, as the function is now not defined inline. Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
In addition, mock the real interaction, even though they were harmless no-ops due to the lack of mocked socket. Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
Remove relevant TODO Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
I've added unit tests for all the interactions now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this, looks very nice. I have a bunch of comments/nitpicks but fine to address in followups I think. Giving people this better user experience seems well worth it!
refreshInfo.TimeRemaining = (maxInhibition - now.Sub(*snapst.RefreshInhibitedTime)).Truncate(time.Second) | ||
// Send the notification asynchronously to avoid holding the state lock. | ||
asyncPendingRefreshNotification(context.TODO(), client, refreshInfo) | ||
// XXX: remove the warning or send it only if no notification was delivered? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think we should remove the warning. Fwiw, I think the warning here is really a bit of a misfeature because we cannot make it go away once the app was refreshed. But it's fine to keep the XXX and do that in a followup. Especially this first warning that just warns for the first time seems a bit unneeded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree but I'd like to remove them in a follow up.
restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { | ||
notificationSent = true | ||
c.Check(refreshInfo.InstanceName, Equals, "pkg") | ||
c.Check(refreshInfo.TimeRemaining, Equals, time.Hour*14*24/2-time.Second) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A comment here might be nice, something like: // just below the 14d window that triggers a stricter warning
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually explained the XXX aspect of this test. Please see inline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this test is assuming that the test itself will always take less than one second? That seems ... brittle ... given our past experiences with running unit tests on arm* LP builders
return err | ||
} | ||
if _, ok := err.(*BusySnapError); ok { | ||
asyncPendingRefreshNotification(context.TODO(), client, refreshInfo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is slightly asymmetric, yes? I mean, we generate a warning here, i.e. the "policy" how strongly we warn is part of "autorefresh.go" here. But for the notifications we just give the "refreshInfo" and leave the "policy" about how strongly it warns to the "client.PendingRefreshNotification".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right but I think this resolves itself when the warnings go away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warnings are removed in a follow-up
@@ -557,9 +583,16 @@ func inhibitRefresh(st *state.State, snapst *SnapState, info *snap.Info, checker | |||
if now.Sub(*snapst.RefreshInhibitedTime) < maxInhibition { | |||
// If we are still in the allowed window then just return | |||
// the error but don't change the snap state again. | |||
refreshInfo.TimeRemaining = (maxInhibition - now.Sub(*snapst.RefreshInhibitedTime)).Truncate(time.Second) | |||
asyncPendingRefreshNotification(context.TODO(), client, refreshInfo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nitpick) we have the notification sending code in very small variations three times here, I wonder if this could be somehow simplified but definitely more a followup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will do this with the removal of the warning, in a follow-up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done (in a follow-up that needs this to land first)
var body string | ||
var icon string | ||
var hints []notification.Hint | ||
if daysLeft := int(refreshInfo.TimeRemaining.Truncate(time.Hour).Hours() / 24); daysLeft > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest forming the parenthesised part of the message independently, then construct the body using Close the app to avoid disruptions (%s)
as the format string. This would remove the need for translators to repeat the same translation 6+ times (depending on how many plural forms the language has).
We've got similar formatting code in timeutil/human.go
for formatting times rather than durations, so perhaps that part of the formatting could be moved there (either in this PR or a follow-up).
I'm iterating on this now, mainly adding tests but not just that. It should be ready today. |
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Instead of opening the snap-store page, it's better to ask the application to close. This will be a follow-up, at least for X11 systems. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Are we green? Super green! Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
This way we detect malformed number of attempts Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've only considered the session agent/notifications sections of the PR in this review.
var body string | ||
var icon string | ||
var hints []notification.Hint | ||
if daysLeft := int(refreshInfo.TimeRemaining.Truncate(time.Hour).Hours() / 24); daysLeft > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest forming the parenthesised part of the message independently, then construct the body using Close the app to avoid disruptions (%s)
as the format string. This would remove the need for translators to repeat the same translation 6+ times (depending on how many plural forms the language has).
We've got similar formatting code in timeutil/human.go
for formatting times rather than durations, so perhaps that part of the formatting could be moved there (either in this PR or a follow-up).
// XXX: how are instances supported in the snap store, are they? | ||
|
||
// TODO: silently ignore error returned when the notification server does not exist. | ||
// TODO: track returned notification ID and respond to actions, if supported. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to have interactivity for FDO notifications, the idle tracking code will need updating to keep the session agent alive until the notification is dismissed. That's more of a TODO item than something that needs to be done in this PR though.
|
||
// TODO: silently ignore error returned when the notification server does not exist. | ||
// TODO: track returned notification ID and respond to actions, if supported. | ||
if _, err := notifySrv.SendNotification(msg); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of the nicer parts of the the new GNOME notification spec: it has client provided notification IDs, so we don't need to track server IDs to update/replace/withdraw old notifications.
notifySrv := notification.New(conn) | ||
|
||
// TODO: this message needs to be crafted better as it's the only thing guaranteed to be delivered. | ||
summary := fmt.Sprintf(i18n.G("Pending update of %q snap"), refreshInfo.InstanceName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given we have the desktop entry of the busy application, perhaps it would be worth including the name from that too? That potentially gives us a localised name. As it is unverified information from the client though, we probably still want to include the snape name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've experimented with some of that from the side of snapd, where we know the app name that is busy. It tends to create repetitive patterns (chrome chrome ...) and I didn't like the result. Desktop file names are sometimes equally weird so I'd postpone that until we can give it a try on a larger sample size.
The connection is opened once and shared within the process. It should not be closed here. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
This way the session agent is not pretending to be the busy snap application but correctly represents itself. The icon is still loaded from the desktop file, if we can connect the dots between the application and the desktop file, so that the icon is familiar to the user. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, I am slightly worried about the brittleness of one test, but can certainly be cleaned up in a followup.
Thanks for all the work here!
restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { | ||
notificationSent = true | ||
c.Check(refreshInfo.InstanceName, Equals, "pkg") | ||
c.Check(refreshInfo.TimeRemaining, Equals, time.Hour*14*24/2-time.Second) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this test is assuming that the test itself will always take less than one second? That seems ... brittle ... given our past experiences with running unit tests on arm* LP builders
}) | ||
} | ||
|
||
func (s *restSuite) TestPostPendingRefreshNotificationNoNotificatinServer(c *C) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func (s *restSuite) TestPostPendingRefreshNotificationNoNotificatinServer(c *C) { | |
func (s *restSuite) TestPostPendingRefreshNotificationNoNotificationServer(c *C) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll send a follow-up for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sent as #9552
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good. I think eventually it would be good to convert over to running against a real (private) session bus. That can come later though.
As with last review, this approval is only taking the session agent and notification portions of the PR into account.
They say a picture is worth a thousand words and today I agree.